昨天提到虛設常式與模擬物件的差異,兩者之間之差在驗證的時候如果是用該假物件驗證,則為模擬物件;反之,則為虛設常式。此外,每一次的測試都應該只有一個關注點(換言之,應只有一個模擬物件),若一個測試含有多個驗證(多個模擬物件),則會引發一些疑慮,舉個簡單的例子如下:
[Test]
public void CheckSumResult()
{
// Arrange
var sum0 = 0;
var sum1 = 0;
var sum2 = 0;
// Act
sum0 = 1001 + 1 + 2;
sum1 = 1 + 1001 + 2;
sum2 = 1 + 2 + 1001;
// Assert
Assert.AreEqual(1004, sum0);
Assert.AreEqual(1004, sum1);
Assert.AreEqual(1004, sum2);
}
OK,寫到這邊可以看出來我們在測試程式碼 CheckSumResult 做了三次的總和,順序互相調換,理論上總和應該都是1004;但假設今天這個互相調換的程式碼沒寫好,變成以下的情況:
[Test]
public void CheckSumResult()
{
// Arrange
var sum0 = 0;
var sum1 = 0;
var sum2 = 0;
// Act
sum0 = 1001 + 1 + 2;
sum1 = 1 + 1 + 2;
sum2 = 1 + 2 + 1001;
// Assert
Assert.AreEqual(1004, sum0);
Assert.AreEqual(1004, sum1);
Assert.AreEqual(1004, sum2);
}
在 sum0 到 sum1 的時候,1001 與 1 應該要互相調換,但不知為何 1001 沒有成功覆寫第二個位置,導致 sum1 的值不符合預期,就會出錯;但是,第一與三個總和應該還是要出現正確,卻無法從這個測試得知。其原因在於 NUnit 的機制是在驗證發生失敗時,會拋出一個 AssertException 例外,NUnit 測試執行器會攔截這個例外,認為目前這個測試方法失敗了,就不會繼續執行下面的程式碼。同理,假設有人把昨天兩個的測試寫在一起,改寫成以下的例子:
using NUnit3;
[TestFixture]
public class EmailWithLogSystemUnitTests
{
[Test]
public void SendFunction_Fail()
{
// Arrange
StubEmailSerivce stubEmailService = new StubEmailSerivce();
FakeLogSerivce mockLogService = new FakeLogSerivce();
EmailWithLogSystem EmailWithLogService = new EmailWithLogSystem(stubEmailService, mockLogService);
// Act
var result = EmailWithLogService.SendFunction("Test@abc.com.tw", "Test Demo");
// Assert
Assert.AreEqual("Fail", result);
Assert.AreEqual("Test@abc.com.tw is not send yet!", mockLogService.logMessage);
}
}
public class StubEmailSerivce : IEmailService
{
public string SendEmail(mailAddress, mailMessage)
{
return "Fail";
}
}
public class FakeLogSerivce : ILogService
{
public string logMessage;
public string Log(string LogMessage)
{
logMessage = LogMessage;
}
}
這段測試碼出現了兩個問題點,第一個是出現多個驗證,這樣的壞處是若改動程式碼,如下:
public class StubEmailSerivce : IEmailService
{
public string SendEmail(mailAddress, mailMessage)
{
// Fail 改成 Success
// return "Fail";
return "Success";
}
}
第一眼看到這段改動,很難在第一時間看出第二個驗證是否有出錯,出錯的問題點在哪。此外,第二個問題點是有假物件同時擔任虛設常式及模擬物件。因有多個驗證,在第一個驗證的時候,他是扮演虛設常式;但在第二個驗證的時候,又擔任了模擬物件,角色呈現曖昧不清的情況,會造成程式碼閱讀上的困難,形成維護上的成本。
在單元測試的藝術中,過度指定是指
對一個測試單元該如何完成內部行為進行了假設,而不是只檢查最終行為的正確性。
好,我相信看到這會覺得好抽象XDDD,先列出單元測試的藝術中提出的幾種情況,再舉個簡單的例子。
那我們以「測試在需要使用虛設常式物件時,使用模擬物件」的情境並搭配昨天的狀況來撰寫,如下:
using NUnit3;
[TestFixture]
public class EmailWithLogSystemUnitTests
{
[Test]
public void SendFunction_CatchSendResult_Success()
{
// Arrange
MockEmailSuccessSerivce mockEmailService = new MockEmailSuccessSerivce();
StubLogSerivce stubLogService = new StubLogSerivce();
EmailWithLogSystem EmailWithLogService = new EmailWithLogSystem(mockEmailService, stubLogService);
// Act
EmailWithLogService.SendFunction("Test@abc.com.tw", "Test Demo");
// Assert
Assert.AreEqual("Success", mockEmailService.SendResult);
}
}
public class MockEmailSuccessSerivce : IEmailService
{
public string SendResult
public string SendEmail(mailAddress, mailMessage)
{
SendResult = "Success";
return "Success";
}
}
public class StubLogSerivce : ILogService
{
public string logMessage;
public string Log(string LogMessage)
{
logMessage = LogMessage;
}
}
昨天提到當我們 SendEmail 的時候,我們去驗證方法提供的回傳值;然而,今天我們卻以模擬物件的寫法去驗證是不是有做 SendEmail 這個動作,當之後若規格發生改變,改變了回傳值,我們也需相對應改模擬物件的方法,這樣使得程式碼維護變困難卻沒有任何測試效益。
好,那接下來又要提更抽象的東西 XD,假設我們今天新增了一個虛設常式,然後這個虛設常式又可以再新增虛設常式,甚至可以新增要驗證的模擬物件,形成一個假物件鏈(這難道是傳說中的假物件俄羅斯娃娃套餐!?XDD)。
舉個例子:
public class EmailWithLogServiceFactory()
{
public class StubEmailSuccessSerivce : IEmailService
{
public string SendEmail(mailAddress, mailMessage)
{
return "Success";
}
}
public class StubEmailFailSerivce : IEmailService
{
public string SendEmail(mailAddress, mailMessage)
{
return "Fail";
}
}
public class StubLogSerivce : ILogService
{
public string logMessage;
public string Log(string LogMessage)
{
logMessage = LogMessage;
}
}
public class MockLogSerivce : ILogService
{
public string logMessage;
public string Log(string LogMessage)
{
logMessage = LogMessage;
}
}
}
可以看出來,我們把 Day-13 所用到的模擬物件都彙整在 EmailWithLogServiceFactory 裡面。實務上,在工廠方法(Factory Method Pattern)的設計框架中,要撰寫測試的話就很容易以這種形式撰寫。因此,會隨著不同的設計方式而決定假物件的設計模式,進而衍生不同的假物件鏈。
到這邊算是把假物件做個簡單的概述,撰寫單元測試的核心很大一部分就是看假物件怎麼設計,而決定了後續假物件的難易程度。相信如果這幾天都有在看的人會發現一件事情,我們花很大的篇幅,在撰寫假物件的程式碼;其實這會衍生很多問題點(擷取自單元測試的藝術):
因此,接下來明天終於要介紹另一個單元測試很大的核心概念——隔離框架(isolation framework),教你如何產製動態虛設常式物件(dynamic stub)和動態模擬物件(dynamic mock)。